// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Mono.Cecil;

namespace Mono.Linker
{
    public class WarningSuppressionWriter
    {
        private readonly Dictionary<AssemblyNameDefinition, HashSet<(int Code, IMemberDefinition Member)>> _warnings;
        private readonly FileOutputKind _fileOutputKind;
        readonly LinkContext _context;

        public WarningSuppressionWriter(LinkContext context, FileOutputKind fileOutputKind = FileOutputKind.CSharp)
        {
            _warnings = new Dictionary<AssemblyNameDefinition, HashSet<(int, IMemberDefinition)>>();
            _fileOutputKind = fileOutputKind;
            _context = context;
        }

        public bool IsEmpty => _warnings.Count == 0;

        public void AddWarning(int code, ICustomAttributeProvider provider)
        {
            // We don't have a targeted suppression mechanism for
            // warnings from assembly-level attributes.
            if (provider is not IMemberDefinition memberDefinition)
                return;

            if (UnconditionalSuppressMessageAttributeState.GetModuleFromProvider(provider) is not ModuleDefinition module)
                return;

            var assemblyName = module.Assembly.Name;
            if (!_warnings.TryGetValue(assemblyName, out var warnings))
            {
                warnings = new HashSet<(int, IMemberDefinition)>();
                _warnings.Add(assemblyName, warnings);
            }

            warnings.Add((code, memberDefinition));
        }

        public void OutputSuppressions(string directory)
        {
            foreach (var assemblyName in _warnings.Keys)
            {
                if (_fileOutputKind == FileOutputKind.Xml)
                {
                    OutputSuppressionsXmlFormat(assemblyName, directory);
                }
                else
                {
                    OutputSuppressionsCSharpFormat(assemblyName, directory);
                }
            }
        }

        void OutputSuppressionsXmlFormat(AssemblyNameDefinition assemblyName, string directory)
        {
            var xmlTree = new XElement("linker");
            var xmlAssembly = new XElement("assembly", new XAttribute("fullname", assemblyName.FullName));
            xmlTree.Add(xmlAssembly);

            foreach (var warning in GetListOfWarnings(assemblyName))
            {
                xmlAssembly.Add(
                    new XElement("attribute",
                        new XAttribute("fullname", "System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute"),
                        new XElement("argument", Constants.ILLink),
                        new XElement("argument", $"IL{warning.Code}"),
                        new XElement("property", new XAttribute("name", UnconditionalSuppressMessageAttributeState.ScopeProperty),
                            GetWarningSuppressionScopeString(warning.MemberDocumentationSignature)),
                        new XElement("property", new XAttribute("name", UnconditionalSuppressMessageAttributeState.TargetProperty),
                            warning.MemberDocumentationSignature)));
            }

            XDocument xdoc = new XDocument(xmlTree);
            using (var xw = XmlWriter.Create(Path.Combine(directory, $"{assemblyName.Name}.WarningSuppressions.xml"),
                new XmlWriterSettings { Indent = true }))
            {
                xdoc.Save(xw);
            }
        }

        void OutputSuppressionsCSharpFormat(AssemblyNameDefinition assemblyName, string directory)
        {
            using (var sw = new StreamWriter(Path.Combine(directory, $"{assemblyName.Name}.WarningSuppressions.cs")))
            {
                StringBuilder sb = new StringBuilder("using System.Diagnostics.CodeAnalysis;").AppendLine().AppendLine();
                foreach (var warning in GetListOfWarnings(assemblyName))
                {
                    sb.Append("[assembly: UnconditionalSuppressMessage(\"")
                        .Append(Constants.ILLink)
                        .Append("\", \"IL").Append(warning.Code)
                        .Append("\", Scope = \"").Append(GetWarningSuppressionScopeString(warning.MemberDocumentationSignature))
                        .Append("\", Target = \"").Append(warning.MemberDocumentationSignature)
                        .AppendLine("\")]");
                }

                sw.Write(sb.ToString());
            }
        }

        List<(int Code, string MemberDocumentationSignature)> GetListOfWarnings(AssemblyNameDefinition assemblyName)
        {
            List<(int Code, string MemberDocumentationSignature)> listOfWarnings = new List<(int Code, string MemberDocumentationSignature)>();
            StringBuilder sb = new StringBuilder();
            foreach (var warning in _warnings[assemblyName].ToList())
            {
                DocumentationSignatureGenerator.VisitMember(warning.Member, sb, _context);
                listOfWarnings.Add((warning.Code, sb.ToString()));
                sb.Clear();
            }

            listOfWarnings.Sort();
            return listOfWarnings;
        }

        static string GetWarningSuppressionScopeString(string memberDocumentationSignature)
        {
            if (memberDocumentationSignature.StartsWith(DocumentationSignatureGenerator.TypePrefix))
                return "type";

            return "member";
        }

        public enum FileOutputKind
        {
            CSharp,
            Xml
        };
    }
}
